#!/bin/sh /etc/rc.common

START=99
STOP=10
USE_PROCD=1

. "$IPKG_INSTROOT/lib/functions/network.sh"
. "$IPKG_INSTROOT/etc/nikki/scripts/include.sh"

extra_command 'update_subscription' 'Update subscription by section id'

boot() {
	# prepare files
	prepare_files
	# load config
	config_load nikki
	# start delay
	local enabled start_delay
	config_get_bool enabled "config" "enabled" 0
	config_get start_delay "config" "start_delay" 0
	if [[ "$enabled" == 1 && "$start_delay" -gt 0 ]]; then
		log "App" "Start after $start_delay seconds."
		sleep "$start_delay"
	fi
	# start
	start
}

start_service() {
	# prepare files
	prepare_files
	# load config
	config_load nikki
	# check if enabled
	local enabled
	config_get_bool enabled "config" "enabled" 0
	if [ "$enabled" == 0 ]; then
		log "App" "Disabled."
		log "App" "Exit."
		return
	fi
	# start
	log "App" "Enabled."
	log "App" "Start."
	# get config
	## app config
	local scheduled_restart cron_expression profile test_profile fast_reload
	config_get_bool scheduled_restart "config" "scheduled_restart" 0
	config_get cron_expression "config" "cron_expression"
	config_get profile "config" "profile"
	config_get_bool test_profile "config" "test_profile" 0
	config_get_bool fast_reload "config" "fast_reload" 0
	## mixin config
	### mixin file content
	local mixin_file_content
	config_get_bool mixin_file_content "mixin" "mixin_file_content" 0
	## environment variable
	local disable_safe_path_check disable_loopback_detector disable_quic_go_gso disable_quic_go_ecn
	config_get_bool disable_safe_path_check "env" "disable_safe_path_check" 0
	config_get_bool disable_loopback_detector "env" "disable_loopback_detector" 0
	config_get_bool disable_quic_go_gso "env" "disable_quic_go_gso" 0
	config_get_bool disable_quic_go_ecn "env" "disable_quic_go_ecn" 0
	# get profile
	if [[ "$profile" == "file:"* ]]; then
		local profile_name; profile_name="${profile/file:/}"
		local profile_file; profile_file="$PROFILES_DIR/$profile_name"
		log "Profile" "Use file: $profile_name."
		if [ ! -f "$profile_file" ]; then
			log "Profile" "File not found."
			log "App" "Exit."
			return
		fi
		cp -f "$profile_file" "$RUN_PROFILE_PATH"
	elif [[ "$profile" == "subscription:"* ]]; then
		local subscription_section; subscription_section="${profile/subscription:/}"
		local subscription_name subscription_prefer
		config_get subscription_name "$subscription_section" "name"
		config_get subscription_prefer "$subscription_section" "prefer" "remote"
		log "Profile" "Use subscription: $subscription_name."
		local subscription_file; subscription_file="$SUBSCRIPTIONS_DIR/$subscription_section.yaml"
		if [ "$subscription_prefer" == "remote" ] || [[ "$subscription_prefer" == "local" && ! -f "$subscription_file" ]]; then
			update_subscription "$subscription_section"
		fi
		if [ ! -f "$subscription_file" ]; then
			log "Profile" "Subscription file not found."
			log "App" "Exit."
			return
		fi
		cp -f "$subscription_file" "$RUN_PROFILE_PATH"
	else
		log "Profile" "No profile/subscription selected."
		log "App" "Exit."
		return
	fi
	# mixin
	log "Mixin" "Mixin config."
	if [ "$mixin_file_content" == 0 ]; then
		ucode -S "$MIXIN_UC" | yq -M -p json -o yaml | yq -M -i ea '... comments="" | . as $item ireduce ({}; . * $item ) | .rules = .nikki-rules + .rules | del(.nikki-rules)' "$RUN_PROFILE_PATH" -
	elif [ "$mixin_file_content" == 1 ]; then
		ucode -S "$MIXIN_UC" | yq -M -p json -o yaml | yq -M -i ea '... comments="" | . as $item ireduce ({}; . * $item ) | .rules = .nikki-rules + .rules | del(.nikki-rules)' "$RUN_PROFILE_PATH" "$MIXIN_FILE_PATH" -
	fi
	# test profile
	if [ "$test_profile" == 1 ]; then
		log "Profile" "Testing..."
		if ($PROG -d "$RUN_DIR" -t >> "$CORE_LOG_PATH" 2>&1); then
			log "Profile" "Test passed."
		else
			log "Profile" "Test failed."
			log "Profile" "Please check the core log to find out the problem."
			log "App" "Exit."
			return
		fi
	fi
	# start core
	log "Core" "Start."
	procd_open_instance nikki

	procd_set_param command /bin/sh -c "$PROG -d $RUN_DIR >> $CORE_LOG_PATH 2>&1"
	procd_set_param file "$RUN_PROFILE_PATH"
	procd_set_param env SKIP_SAFE_PATH_CHECK="$disable_safe_path_check" DISABLE_LOOPBACK_DETECTOR="$disable_loopback_detector" QUIC_GO_DISABLE_GSO="$disable_quic_go_gso" QUIC_GO_DISABLE_ECN="$disable_quic_go_ecn"
	if [ "$fast_reload" == 1 ]; then
		procd_set_param reload_signal HUP
	fi
	procd_set_param pidfile "$PID_FILE_PATH"
	procd_set_param respawn
	procd_set_param limits core="unlimited" nofile="1048576 1048576"

	procd_close_instance
	# cron
	if [[ "$scheduled_restart" == 1 && -n "$cron_expression" ]]; then
		log "App" "Set scheduled restart."
		echo "$cron_expression /etc/init.d/nikki restart #nikki" >> "/etc/crontabs/root"
		/etc/init.d/cron restart
	fi
	# set started flag
	touch "$STARTED_FLAG_PATH"
}

service_started() {
	# check if started
	if [ ! -f "$STARTED_FLAG_PATH" ]; then
		return
	fi
	# load config
	config_load nikki
	# check if proxy enabled
	local enabled
	config_get_bool enabled "proxy" "enabled" 0
	if [ "$enabled" == 0 ]; then
		log "Proxy" "Disabled."
		return
	fi
	# get config
	## mixin
	### tun
	local tun_device
	config_get tun_device "mixin" "tun_device" "nikki"
	## proxy config
	### general
	local tcp_mode udp_mode ipv4_proxy ipv6_proxy
	config_get tcp_mode "proxy" "tcp_mode"
	config_get udp_mode "proxy" "udp_mode"
	config_get_bool ipv4_proxy "proxy" "ipv4_proxy" 0
	config_get_bool ipv6_proxy "proxy" "ipv6_proxy" 0
	# prepare config
	local tproxy_enable; tproxy_enable=0
	if [[ "$tcp_mode" == "tproxy" || "$udp_mode" == "tproxy" ]]; then
		tproxy_enable=1
	fi
	local tun_enable; tun_enable=0
	if [[ "$tcp_mode" == "tun" || "$udp_mode" == "tun" ]]; then
		tun_enable=1
	fi
	# fix compatible with dockerd
	## cgroupfs-mount
	### when cgroupfs-mount is installed, cgroupv1 will mounted instead of cgroupv2, we need to create cgroup manually
	if (mount | grep -q -w "^cgroup"); then
		mkdir -p "/sys/fs/cgroup/net_cls/$CGROUP_NAME"
		echo "$CGROUP_ID" > "/sys/fs/cgroup/net_cls/$CGROUP_NAME/net_cls.classid"
		cat "$PID_FILE_PATH" > "/sys/fs/cgroup/net_cls/$CGROUP_NAME/cgroup.procs"
	fi
	## kmod-br-netfilter
	### when kmod-br-netfilter is loaded, bridge-nf-call-iptables and bridge-nf-call-ip6tables are set to 1, we need to set them to 0 if tproxy is enabled
	if [ "$tproxy_enable" == 1 ] && (lsmod | grep -q br_netfilter); then
		if [ "$ipv4_proxy" == 1 ]; then
			local bridge_nf_call_iptables; bridge_nf_call_iptables=$(sysctl -e -n net.bridge.bridge-nf-call-iptables)
			if [ "$bridge_nf_call_iptables" == 1 ]; then
				touch "$BRIDGE_NF_CALL_IPTABLES_FLAG_PATH"
				sysctl -q -w net.bridge.bridge-nf-call-iptables=0
			fi
		fi
		if [ "$ipv6_proxy" == 1 ]; then
			local bridge_nf_call_ip6tables; bridge_nf_call_ip6tables=$(sysctl -e -n net.bridge.bridge-nf-call-ip6tables)
			if [ "$bridge_nf_call_ip6tables" == 1 ]; then
				touch "$BRIDGE_NF_CALL_IP6TABLES_FLAG_PATH"
				sysctl -q -w net.bridge.bridge-nf-call-ip6tables=0
			fi
		fi
	fi
	# proxy
	log "Proxy" "Enabled."
	# wait for tun device online
	if [ "$tun_enable" == 1 ]; then
		log "Proxy" "Waiting for tun device online..."
		local tun_timeout; tun_timeout=15
		local tun_interval; tun_interval=1
		while [ "$tun_timeout" -gt 0 ]; do
			if (ip link show dev "$tun_device" > /dev/null 2>&1); then
				if [ $(ip -json addr show dev "$tun_device" | tun_device="$tun_device" yq -M '.[] | select(.ifname = strenv(tun_device)) | .addr_info | length') -gt 0 ]; then
					log "Proxy" "Tun device is online."
					break
				fi
			fi
			tun_timeout=$((tun_timeout - tun_interval))
			sleep "$tun_interval"
		done
		if [ "$tun_timeout" -le 0 ]; then
			log "Proxy" "Waiting timeout, tun device is not online."
			log "App" "Exit."
			return
		fi
	fi
	# ip route and rule
	if [ "$tproxy_enable" == 1 ]; then
		if [ "$ipv4_proxy" == 1 ]; then
			ip -4 route add local default dev lo table "$TPROXY_ROUTE_TABLE"
			ip -4 rule add pref "$TPROXY_RULE_PREF" fwmark "$TPROXY_FW_MARK" table "$TPROXY_ROUTE_TABLE"
		fi
		if [ "$ipv6_proxy" == 1 ]; then
			ip -6 route add local default dev lo table "$TPROXY_ROUTE_TABLE"
			ip -6 rule add pref "$TPROXY_RULE_PREF" fwmark "$TPROXY_FW_MARK" table "$TPROXY_ROUTE_TABLE"
		fi
	fi
	if [ "$tun_enable" == 1 ]; then
		if [ "$ipv4_proxy" == 1 ]; then
			ip -4 route add unicast default dev "$tun_device" table "$TUN_ROUTE_TABLE"
			ip -4 rule add pref "$TUN_RULE_PREF" fwmark "$TUN_FW_MARK" table "$TUN_ROUTE_TABLE"
		fi
		if [ "$ipv6_proxy" == 1 ]; then
			ip -6 route add unicast default dev "$tun_device" table "$TUN_ROUTE_TABLE"
			ip -6 rule add pref "$TUN_RULE_PREF" fwmark "$TUN_FW_MARK" table "$TUN_ROUTE_TABLE"
		fi
		$FIREWALL_INCLUDE_SH
	fi
	# hijack
	utpl -D cgroup_name="$CGROUP_NAME" -D cgroup_id="$CGROUP_ID" -D tproxy_fw_mark="$TPROXY_FW_MARK" -D tun_fw_mark="$TUN_FW_MARK" -S "$HIJACK_UT" | nft -f -
	# check hijack
	if (nft list tables | grep -q nikki); then
		log "Proxy" "Hijack successful."
	else
		log "Proxy" "Hijack failed."
		log "App" "Exit."
	fi
}

service_stopped() {
	cleanup
}

reload_service() {
	cleanup
	start
}

service_triggers() {
	procd_add_reload_trigger "nikki"
}

cleanup() {
	# clear log
	clear_log
	# delete routing policy
	ip -4 rule del table "$TPROXY_ROUTE_TABLE" > /dev/null 2>&1
	ip -4 rule del table "$TUN_ROUTE_TABLE" > /dev/null 2>&1
	ip -6 rule del table "$TPROXY_ROUTE_TABLE" > /dev/null 2>&1
	ip -6 rule del table "$TUN_ROUTE_TABLE" > /dev/null 2>&1
	# delete routing table
	ip -4 route flush table "$TPROXY_ROUTE_TABLE" > /dev/null 2>&1
	ip -4 route flush table "$TUN_ROUTE_TABLE" > /dev/null 2>&1
	ip -6 route flush table "$TPROXY_ROUTE_TABLE" > /dev/null 2>&1
	ip -6 route flush table "$TUN_ROUTE_TABLE" > /dev/null 2>&1
	# delete hijack
	nft delete table inet nikki > /dev/null 2>&1
	local handles handle
	handles=$(nft --json list table inet fw4 | yq -M '.nftables[] | select(has("rule")) | .rule | select(.chain == "input" and .comment == "nikki") | .handle')
	for handle in $handles; do
		nft delete rule inet fw4 input handle "$handle"
	done
	handles=$(nft --json list table inet fw4 | yq -M '.nftables[] | select(has("rule")) | .rule | select(.chain == "forward" and .comment == "nikki") | .handle')
	for handle in $handles; do
		nft delete rule inet fw4 forward handle "$handle"
	done
	# delete started flag
	rm "$STARTED_FLAG_PATH" > /dev/null 2>&1
	# revert fix compatible with dockerd
	## kmod-br-netfilter
	if (rm "$BRIDGE_NF_CALL_IPTABLES_FLAG_PATH" > /dev/null 2>&1); then
		sysctl -q -w net.bridge.bridge-nf-call-iptables=1
	fi
	if (rm "$BRIDGE_NF_CALL_IP6TABLES_FLAG_PATH" > /dev/null 2>&1); then
		sysctl -q -w net.bridge.bridge-nf-call-ip6tables=1
	fi
	# delete cron
	sed -i "/#nikki/d" "/etc/crontabs/root" > /dev/null 2>&1
	/etc/init.d/cron restart
}

update_subscription() {
	local subscription_section; subscription_section="$1"
	if [ -z "$subscription_section" ]; then
		return
	fi
	# load config
	config_load nikki
	# get subscription config
	local subscription_name subscription_url subscription_user_agent
	config_get subscription_name "$subscription_section" "name"
	config_get subscription_url "$subscription_section" "url"
	config_get subscription_user_agent "$subscription_section" "user_agent"
	# reset subscription info
	uci_remove "nikki" "$subscription_section" "expire"
	uci_remove "nikki" "$subscription_section" "upload"
	uci_remove "nikki" "$subscription_section" "download"
	uci_remove "nikki" "$subscription_section" "total"
	uci_remove "nikki" "$subscription_section" "used"
	uci_remove "nikki" "$subscription_section" "avaliable"
	uci_remove "nikki" "$subscription_section" "update"
	uci_remove "nikki" "$subscription_section" "success"
	# update subscription
	log "Profile" "Update subscription: $subscription_name."
	local subscription_header_tmpfile; subscription_header_tmpfile="$TEMP_DIR/$subscription_section.header"
	local subscription_tmpfile; subscription_tmpfile="$TEMP_DIR/$subscription_section.yaml"
	local subscription_file; subscription_file="$SUBSCRIPTIONS_DIR/$subscription_section.yaml"
	if (curl -s -f --connect-timeout 15 --retry 3 -L -X GET -A "$subscription_user_agent" -D "$subscription_header_tmpfile" -o "$subscription_tmpfile" "$subscription_url"); then
		log "Profile" "Subscription update successful."
		local subscription_expire subscription_upload subscription_download subscription_total subscription_used subscription_avaliable
		subscription_expire=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -o -E "expire=[[:digit:]]+" | cut -d '=' -f 2)
		subscription_upload=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -o -E "upload=[[:digit:]]+" | cut -d '=' -f 2)
		subscription_download=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -o -E "download=[[:digit:]]+" | cut -d '=' -f 2)
		subscription_total=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -o -E "total=[[:digit:]]+" | cut -d '=' -f 2)
		if [[ -n "$subscription_upload" && -n "$subscription_download" ]]; then
			subscription_used=$((subscription_upload + subscription_download))
			if [ -n "$subscription_total" ]; then
				subscription_avaliable=$((subscription_total - subscription_upload - subscription_download))
			fi
		fi
		# update subscription info
		if [ -n "$subscription_expire" ]; then
			uci_set "nikki" "$subscription_section" "expire" "$(date "+%Y-%m-%d %H:%M:%S" -d @$subscription_expire)"
		fi
		if [ -n "$subscription_upload" ]; then
			uci_set "nikki" "$subscription_section" "upload" "$(format_filesize $subscription_upload)"
		fi
		if [ -n "$subscription_download" ]; then
			uci_set "nikki" "$subscription_section" "download" "$(format_filesize $subscription_download)"
		fi
		if [ -n "$subscription_total" ]; then
			uci_set "nikki" "$subscription_section" "total" "$(format_filesize $subscription_total)"
		fi
		if [ -n "$subscription_used" ]; then
			uci_set "nikki" "$subscription_section" "used" "$(format_filesize $subscription_used)"
		fi
		if [ -n "$subscription_avaliable" ]; then
			uci_set "nikki" "$subscription_section" "avaliable" "$(format_filesize $subscription_avaliable)"
		fi
		uci_set "nikki" "$subscription_section" "update" "$(date "+%Y-%m-%d %H:%M:%S")"
		uci_set "nikki" "$subscription_section" "success" "1"
		# update subscription file
		rm -f "$subscription_header_tmpfile"
		mv -f "$subscription_tmpfile" "$subscription_file"
	else
		log "Profile" "Subscription update failed."
		# update subscription info
		uci_set "nikki" "$subscription_section" "success" "0"
		# remove tmpfile
		rm -f "$subscription_header_tmpfile"
		rm -f "$subscription_tmpfile"
	fi
	uci_commit "nikki"
}
